In [6]:
from scipy import *
from pylab import *
from pandas import *
%matplotlib inline
matplotlib.rcParams['figure.figsize'] = [10,8]
In [128]:
def interpolate(a, b, frac):
return (b - a) * frac + a
class Point3:
"""3D point"""
def __init__(self, x, y, z):
self.x = x
self.y = y
self.z = z
@staticmethod
def interpolate(a, b, frac):
return Point3(interpolate(a.x, b.x, frac),
interpolate(a.y, b.y, frac),
interpolate(a.z, b.z, frac))
def subtract(self, rhs):
return Point3(self.x - rhs.x,
self.y - rhs.y,
self.z - rhs.z)
def length(self):
return sqrt(self.x * self.x + self.y * self.y + self.z * self.z)
def eval(self, u):
return Point3(self.x.eval(u), self.y.eval(u), self.z.eval(u))
def __repr__(self):
return "(%g,%g,%g)" % (self.x, self.y, self.z)
class Line2:
"""y = ax + b"""
def __init__(self, a, b):
self.a = a
self.b = b
@staticmethod
def from_intercepts(x1, y1, x2, y2):
# y1 = a x1 + b
# y2 = a x2 + b
# y1 - a x1 = y2 - a x2
# a x1 - a x2 = y1 - y2
# a (x1 - x2) = y1 - y2
# a = (y1 - y2) / (x1 - x2)
a = (y1 - y2) / float(x1 - x2)
b = y1 - a * x1
return Line2(a, b)
def eval(self, x):
return self.a * x + self.b
def __repr__(self):
return "(%gx+%g)" % (self.a, self.b)
class Parabola2:
# Simplified parabola y = ax^2 + c
# Vertex is at (0, c)
# Intersects (+-1, c + a)
def __init__(self, a, c):
self.a = a
self.c = c
def eval(self, x):
return self.a * x ** 2 + self.c
def __repr__(self):
return "(%g(x^2)+%g)" % (self.a, self.c)
# Compute "zoom length" of segment from p1 to p1 + frac * p2 - p1
# Zoom length is path length scaled inversely with z,
# since apparent motion on screen also varies inversely with z
def fractionalZoomLength(p1, p2, frac):
length = p1.subtract(p2).length()
dz = p2.z - p1.z
if fabs(dz) < 1e-10:
return frac * length / p1.z;
else:
return length / dz * (log(dz * frac + p1.z) - log(p1.z))
# Compute "zoom length" of segment from p1 to p2.
# Zoom length is path length scaled inversely with z,
# since apparent motion on screen also varies inversely with z
def zoomLength(p1, p2):
return fractionalZoomLength(p1, p2, 1)
# Compute point that's dist zoom distance from p1 along the path to p2.
def zoomInterpolate(p1, p2, dist):
length = p1.subtract(p2).length()
dz = p2.z - p1.z
if fabs(dz) < 1e-10:
frac = dist / length * p1.z
else:
frac = (exp(dz * dist / length + log(p1.z)) - p1.z) / dz
return Point3.interpolate(p1, p2, frac)
def testZoomInterpolate(p1, p2, dist):
interp = zoomInterpolate(p1, p2, dist)
print "Requested dist %g actual %g" % (dist, zoomLength(p1, interp))
In [129]:
testZoomInterpolate(Point3(0,0,2), Point3(2,2,2.0001), 1)
In [130]:
testZoomInterpolate(Point3(0,0,2), Point3(1,1,5), 1.2)
In [131]:
testZoomInterpolate(Point3(1,1,5), Point3(0,0,2), .3)
In [132]:
testZoomInterpolate(Point3(5,6,7), Point3(10,20,30), 1)
In [299]:
def computePath(a, b):
"""Compute path from a to b and return as a list of points"""
# First, compute a "context point" from which both a and b are visible
xydist = sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2)
zdist = fabs(a.z - b.z)
ctx_height = 0.5 * (xydist - zdist)
if ctx_height > 0:
# Zoom out from a to the context point (parabola vertex) and then zoom in to b
ctx_z = ctx_height + max(a.z, b.z)
ctx = Point3.interpolate(a, b, (ctx_z - a.z) / xydist)
ctx.z = ctx_z
p1 = Point3(Line2.from_intercepts(-1, a.x, 0, ctx.x), # (-1, a.x) - (0, ctx.x)
Line2.from_intercepts(-1, a.y, 0, ctx.y), # (-1, a.y) - (0, ctx.y)
Parabola2(a.z - ctx.z, ctx.z)) # (-1, a.z) - (0, ctx.z)
p2 = Point3(Line2.from_intercepts(0, ctx.x, 1, b.x), # (0, ctx.x) - (1, b.x)
Line2.from_intercepts(0, ctx.y, 1, b.y), # (0, ctx.y) - (1, b.y)
Parabola2(b.z - ctx.z, ctx.z)) # (0, ctx.z) - (1, b.z)
return [(p1 if u < 0 else p2).eval(u) for u in linspace(-1, 1, 21)]
elif 2 * xydist > zdist + 1e-10:
# No context point, but can follow parabolic zoom in or out without vertex
if a.z < b.z:
# This code only works for zooming in; convert zooming out to zooming in
return list(reversed(computePath(b, a)))
c = (xydist ** 2) / (2.0 * xydist - zdist)
p = Point3(Line2.from_intercepts(c - xydist, a.x, c, b.x),
Line2.from_intercepts(c - xydist, a.y, c, b.y),
Parabola2(-1.0 / c, c + b.z))
return [p.eval(u) for u in linspace(c - xydist, c, 11)]
else:
# Zoom is much larger than translation; go in a straight line
return [a, b]
In [300]:
# Zooming out
path = computePath(Point3(2, 1, 1), Point3(6, 2, 2))
plot([pt.x for pt in path], [pt.z for pt in path])
plot([pt.y for pt in path], [pt.z for pt in path])
Out[300]:
In [301]:
# Zooming in
path = computePath(Point3(2, 1, 2), Point3(6, 2, 1))
plot([pt.x for pt in path], [pt.z for pt in path])
plot([pt.y for pt in path], [pt.z for pt in path])
Out[301]:
In [302]:
# Zoom in
path = computePath(Point3(2, 1, 14.7), Point3(3, 2, 13))
plot([pt.x for pt in path], [pt.z for pt in path])
plot([pt.y for pt in path], [pt.z for pt in path])
Out[302]:
In [303]:
# Zoom out
path = computePath(Point3(2, 1, 13), Point3(3, 2, 14.7))
plot([pt.x for pt in path], [pt.z for pt in path])
plot([pt.y for pt in path], [pt.z for pt in path])
Out[303]:
In [304]:
path = computePath(Point3(2, 1, 20), Point3(6, 2, 2))
plot([pt.x for pt in path], [pt.z for pt in path])
plot([pt.y for pt in path], [pt.z for pt in path])
Out[304]:
In [305]:
path = computePath(Point3(2, 1, 20), Point3(2, 1, 2))
plot([pt.x for pt in path], [pt.z for pt in path])
plot([pt.y for pt in path], [pt.z for pt in path])
path
Out[305]:
In [306]:
path = computePath(Point3(0, 0, 14), Point3(0, 1, 13))
plot([pt.x for pt in path], [pt.z for pt in path])
plot([pt.y for pt in path], [pt.z for pt in path])
Out[306]:
In [306]: